#pragma once

#include "evnet/evnet.hpp"
#include "event2/buffer.h"

namespace evnet {

class buffer
{
public:
    typedef evbuffer* handle_type;

private:

    std::pair<bool, handle_type> handle_{false, nullptr};

    // создание
    static handle_type create_buffer()
    {
        handle_type result = evbuffer_new();
        if (!result)
            throw std::runtime_error("evbuffer_new");

        return result;
    }

    static std::pair<bool, handle_type> create()
    {
        return std::make_pair(true, create_buffer());
    }

    static std::pair<bool, handle_type> assign(handle_type handle)
    {
        if (!handle)
            throw std::runtime_error("assign invalid buffer handle");

        return std::make_pair(false, handle);
    }

    void destroy() throw()
    {
        // удаляем только если сами создавали
        if (handle_.first)
            evbuffer_free(handle_.second);
    }

public:
    // нельзя копировать!!!
    buffer(const buffer&) = delete;
    buffer& operator=(const buffer& buffer) = delete;

    buffer()
        : handle_(create())
    {   }

    explicit buffer(handle_type handle)
        : handle_(assign(handle))
    {   }

    buffer(buffer&& other) throw()
    {
        std::swap(handle_, other.handle_);
    }

    buffer& operator=(buffer&& other) throw()
    {
        if (this != &other)
            std::swap(handle_, other.handle_);

        return *this;
    }

    handle_type handle() const throw()
    {
        return handle_.second;
    }

    void clear()
    {
        // если буфер внешний - генерируем исключение
        if (!handle_.first)
            throw std::runtime_error("foreign buffer handle");

        // если не пустой
        if (!empty())
        {
            // создаем новый
            auto handle = create();

            // удаляем старый
            destroy();

            // меняем указатели
            handle_ = handle;
        }
    }

    ~buffer() throw()
    {
        destroy();
    }

    void add_file(int fd, ev_off_t offset, ev_off_t length)
    {
        int result = evbuffer_add_file(handle(), fd, offset, length);
        if (result == -1)
            throw std::runtime_error("evbuffer_add_file");
    }

    void add_buffer(buffer buf)
    {
        int result = evbuffer_add_buffer(handle(), buf.handle());
        if (result == -1) // печально будет если исключение
            throw std::runtime_error("evbuffer_add_buffer");
    }

    void append(const void *data, std::size_t len, bool ref = false)
    {
        if (ref)
        {
            int result = evbuffer_add_reference(handle(), data, len, 0, 0);
            if (result == -1)
                throw std::runtime_error("evbuffer_add_reference");
        }
        else
        {
            int result = evbuffer_add(handle(), data, len);
            if (result == -1)
                throw std::runtime_error("evbuffer_add");
        }
    }

    void append(const void *data, std::size_t len,
                evbuffer_ref_cleanup_cb cleanupfn, void *cleanupfn_arg)
    {
        int result = evbuffer_add_reference(handle(), data, len,
                                            cleanupfn, cleanupfn_arg);
        if (result == -1)
            throw std::runtime_error("evbuffer_add_reference");
    }

    void append(const std::string& data, bool ref = false)
    {
        append(data.c_str(), data.size(), ref);
    }

    void prepend(const void *data, std::size_t len)
    {
        int result = evbuffer_prepend(handle(), data, len);
        if (result == -1)
            throw std::runtime_error("evbuffer_prepend");
    }

    void prepend(const std::string& data)
    {
        prepend(data.c_str(), data.size());
    }

    void drain(std::size_t len)
    {
        int result = evbuffer_drain(handle(), len);
        if (result == -1)
            throw std::runtime_error("evbuffer_drain");
    }

    std::size_t copyout(void *out, std::size_t len) const
    {
        ev_ssize_t result = evbuffer_copyout(handle(), out, len);
        if (result == -1)
            throw std::runtime_error("evbuffer_copyout");

        return static_cast<std::size_t>(result);
    }

    unsigned char* pullup(ev_ssize_t len)
    {
        return evbuffer_pullup(handle(), len);
    }

    std::size_t size() const throw()
    {
        return evbuffer_get_length(handle());
    }

    bool empty() const throw()
    {
        return size() == 0;
    }

    std::size_t remove(void *out, std::size_t len) const
    {
        ev_ssize_t result = evbuffer_remove(handle(), out, len);
        if (result == -1)
            throw std::runtime_error("evbuffer_remove");

        return static_cast<std::size_t>(result);
    }
};

} // namespace evnet
